在 async/await 滿天飛的.net core or .net 6 的專案,前陣子有人問到一個問題,她在鎖定同一時間只能一個人上傳檔案的時候,ReaderWriterLockSlim 無法解鎖。
在解鎖的時候會跳錯出錯誤[The write lock is being released without being held.] 這是什麼原因呢?請讓我們繼續看下去...
發生錯誤的程式碼
首先我們先上一段 Code ,這是一個 .net 6 的上傳檔案的API,做的事情都很單純,鎖定執行序然後寫入檔案,就回傳成功!
private static ReaderWriterLockSlim _readerWriterLockSlim = new ReaderWriterLockSlim();
[HttpPost]
[Route("Upload")]
public async Task<IActionResult> UploadFile(IFormFile file)
{
try
{
// 鎖定
if (!_readerWriterLockSlim.TryEnterWriteLock(50))
{
throw new Exception("Be Locked");
}
try
{
// 儲存上傳檔案
var filePath = $"{Directory.GetCurrentDirectory()}/File/";
if (!Directory.Exists(filePath))
{
Directory.CreateDirectory(filePath);
}
var path = $"{filePath}{file.Name}{DateTime.Now:yyyyMMddHHmmssfff}";
await using (Stream stream = new FileStream(path, FileMode.Create))
{
// 重點問題在這行
await file.CopyToAsync(stream);
}
return Ok("Success");
}
finally
{
// 會出錯的地方
_readerWriterLockSlim.ExitWriteLock();
}
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
這段程式執行後會收到一個 Exception : [The write lock is being released without being held.] ,根據說明是這個鎖已經被解掉,但其實沒有解鎖,當你在上傳第二個檔案的時候,會得到被鎖定中的結果。
錯誤發生的原因:
會發生這件事情的主要原因是出在 [await file.CopyToAsync(stream);] 這行,你進來的執行序,執行到這邊的時候會把任務交給 IO Thread,原執行序會釋放掉,當 IO Thread完成的他的任務,會交由空著的執行序接手,通常不會是原本的那條執行序,因此我們可以得到一個結論,當 await 離開原執行序後回來就會換了一條新的執行序,更換執行序這件事情我們先稱之為「Thread-affine」。
但這會對我們造成什麼影響呢?在跟執行序無關的程式都不會有任何影響,只是執行序的ID改變,但 ReaderWriterLockSlim 的 TryEnterWriteLock 與 ExitWriteLock 是會根據執行序作判斷的,當你換了一條執行序回來之後,Exit 會判斷這條執行序沒有相應的 Lock,所以無法被釋放,但你原先執行序的鎖還在,於是導致沒有人可以進來的窘境。
解決方式:
使用 AsyncReaderWriterLock 需安裝 Nuget 套件 Nito.AsyncEx ,程式碼如下:
private static AsyncReaderWriterLock _asyncReaderWriterLock = new AsyncReaderWriterLock();
[HttpPost]
[Route("Upload")]
public async Task<IActionResult> UploadFileV2(IFormFile file)
{
try
{
using (var writerLockAsync = await _asyncReaderWriterLock.WriterLockAsync())
{
var filePath = $"{Directory.GetCurrentDirectory()}/File/";
if (!Directory.Exists(filePath))
{
Directory.CreateDirectory(filePath);
}
Stream stream =
new FileStream($"{filePath}{file.Name}{DateTime.Now:yyyyMMddHHmmssfff}",
FileMode.Create);
var currentProcessorId = Thread.GetCurrentProcessorId();
await file.CopyToAsync(stream);
var currentProcessorIad = Thread.GetCurrentProcessorId();
stream.Close();
}
return Ok("Success");
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
特別注意有很多執行序 Lock 都會遇到這個問題,使用Lock的時候還要多注意。
參考:
https://devblogs.microsoft.com/pfxteam/building-async-coordination-primitives-part-7-asyncreaderwriterlock/
https://github.com/StephenCleary/AsyncEx